Skip to content

feat: pr deployed tracking + many misc changes#76

Merged
waltergalvao merged 16 commits intomainfrom
feat/pr-tracking-deployment-fields
Feb 19, 2026
Merged

feat: pr deployed tracking + many misc changes#76
waltergalvao merged 16 commits intomainfrom
feat/pr-tracking-deployment-fields

Conversation

@waltergalvao
Copy link
Copy Markdown
Contributor

@waltergalvao waltergalvao commented Feb 18, 2026

Greptile Summary

This PR adds deployment tracking for pull requests — recording when a PR was first deployed and computing the time-to-deploy metric (time between merge and deployment). It also exposes two previously-computed-but-unexposed fields (timeToCode, firstDeployedAt) via the GraphQL API and updates all frontend queries and timeline UI to display them.

Key changes:

  • New updatePullRequestDeploymentTracking function in deployment-pr-linking.service.ts that updates firstDeployedAt and timeToDeploy on PR tracking records after a deployment, with idempotency (skips if a prior deployment is earlier).
  • Refactored deployment worker to fetch the PR from DB by ID (with tracking relation) instead of receiving the full serialized PR object in the job payload — more robust against stale data.
  • fetchPullRequestMergeCommitSha added as a fallback REST API call when the webhook doesn't provide the merge commit SHA (e.g., during batch resyncs).
  • Transformer and calculateTimeForEvent refactored with a new uselessAfterMerge flag to correctly suppress live-timer calculations for metrics that don't apply after merge (review, approval, merge times) while enabling them for deploy and cycle time.
  • Frontend timeline now shows "Coding" and "Deploy" steps, with deploy badges and color-coded thresholds.
  • DORA metrics UI fixes: corrected arrow direction logic for higherIsBetter, fixed operator precedence bug in deployment frequency previousAmount, and switched MTTR to abbreviated duration format.
  • Logger enhancement to filter sensitive/large fields from workspace and pullRequest objects before logging.
  • Dev QoL: isLive guard on resync cooldown to allow unlimited resyncs in development.

Confidence Score: 4/5

  • This PR is safe to merge with minor issues flagged in prior review threads that should be addressed.
  • The core deployment tracking logic is well-structured with proper idempotency checks, error handling via captureException, and data integrity validations. The refactoring to pass PR IDs instead of full objects through job queues is a solid improvement. Prior review threads flagged logger mutation, stale field names in loggableFields, and the timeToDeploy live-timer behavior for open PRs — these are the main items preventing a 5/5 score. The new code introduced in this PR is clean and follows existing patterns.
  • apps/api/src/lib/logger.ts (issues flagged in prior threads), apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts (timeToDeploy live-timer for open PRs flagged in prior thread)

Important Files Changed

Filename Overview
apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Adds updatePullRequestDeploymentTracking function and renames findPullRequestsByCommitHashes to findMergedPullRequestsByCommitHashes with mergedAt filter. Includes proper error handling and early-return when previous deployment is earlier.
apps/api/src/app/deployment/services/deployment-create-from-merge.service.ts Adds call to updatePullRequestDeploymentTracking after linking PRs to deployment. Clean integration of new deploy tracking.
apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts Refactored to fetch PR from DB by ID instead of receiving the full object in the job data. Adds validation and includes tracking relation. Contains a type assertion that is safe due to prior null check.
apps/api/src/app/github/services/github-pull-request-tracking.service.ts Adds getTimeToDeploy function and updates getTimeToMerge to accept firstReadyAt parameter in its fallback chain. JSDoc for getTimeToMerge is now slightly outdated.
apps/api/src/app/github/services/github-pull-request.service.ts Adds fetchPullRequestMergeCommitSha with try/catch error handling. Falls back to REST API when webhook data is unavailable. Also adds captureException to existing getFirstCommit error handler and passes firstReadyAt to getTimeToMerge.
apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts Major refactor of transformPullRequestTracking and calculateTimeForEvent to support deploy tracking and new uselessAfterMerge flag. Adds timeToCode, timeToDeploy, and firstDeployedAt fields. Changes live-timer behavior for merged PRs.
apps/api/src/lib/logger.ts Adds field filtering for workspace and pullRequest objects to reduce log noise. Creates a shallow copy to avoid mutating caller's object. Issues with incorrect field names and unsafe property access were flagged in prior review threads.
apps/web/src/app/metrics-and-insights/page.tsx Fixes deployment frequency previousAmount display, capitalizes metric names, and switches MTTR to getAbbreviatedDuration. Previous operator precedence bug was fixed.
apps/web/src/components/card-pull-request/timeline-pull-request.tsx Adds "Coding" and "Deploy" steps to the PR timeline. Refactors getColor to use string-based level names. Uses isNumber guards for duration display.
apps/web/src/components/card-pull-request/use-badges.tsx Adds getDeployBadge function for deploy status badges with time-based thresholds. Only shows badge for merged and deployed PRs.

Sequence Diagram

sequenceDiagram
    participant Webhook as GitHub Webhook
    participant SyncWorker as syncPullRequestWorker
    participant PRService as github-pull-request.service
    participant DeployWorker as deploymentTriggeredByPullRequestMergeWorker
    participant DB as Database
    participant PRLinking as deployment-pr-linking.service
    participant CreateMerge as deployment-create-from-merge.service

    Webhook->>SyncWorker: PR merged event
    SyncWorker->>PRService: syncPullRequest(webhookMergeCommitSha)
    PRService->>PRService: fetchPullRequestMergeCommitSha (fallback if no webhook SHA)
    PRService->>DB: upsertPullRequest (with mergeCommitSha)
    PRService->>DB: upsertPullRequestTracking (timeToMerge with firstReadyAt)
    SyncWorker->>DeployWorker: enqueue(pullRequestId)

    DeployWorker->>DB: findPullRequestById (include tracking)
    DeployWorker->>CreateMerge: createDeploymentFromPullRequestMerge
    CreateMerge->>DB: upsertDeployment
    CreateMerge->>PRLinking: linkPullRequestsToDeployment
    CreateMerge->>PRLinking: updatePullRequestDeploymentTracking
    PRLinking->>DB: Update firstDeployedAt & timeToDeploy

    Note over PRLinking: Auto-linking path (external deployments)
    PRLinking->>DB: findMergedPullRequestsByCommitHashes
    PRLinking->>PRLinking: filterPullRequestsBySubdirectory
    PRLinking->>DB: linkPullRequestsToDeployment
    PRLinking->>DB: Update firstDeployedAt & timeToDeploy (per PR)
Loading

Last reviewed commit: 494f742

@sweetr-dev sweetr-dev Bot added the large Large PR - Consider splitting up into smaller PRs to reduce risk and review time label Feb 18, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

Walkthrough

Adds deployment-aware pull request tracking: new types and API to update PR tracking on deployment, call-site integration when creating deployments from merged PRs, merge-commit propagation changes, new timing metrics (timeToCode, timeToDeploy, firstDeployedAt) surfaced through API and UI.

Changes

Cohort / File(s) Summary
Deployment tracking & linking
apps/api/src/app/deployment/services/deployment-create-from-merge.service.ts, apps/api/src/app/deployment/services/deployment-create-from-merge.types.ts, apps/api/src/app/deployment/services/deployment-pr-linking.service.ts, apps/api/src/app/deployment/services/deployment-pr-linking.types.ts
Added updatePullRequestDeploymentTracking and its types; call added after PR→deployment linking; renamed PR lookup to findMergedPullRequestsByCommitHashes; added data-integrity guards, tracking updates, and logging/capture paths; types now include PullRequestTracking.
Monorepo PR filtering & types
apps/api/src/app/deployment/services/deployment-monorepo.service.ts, apps/api/src/app/deployment/services/deployment-monorepo.types.ts
Made filterPullRequestsBySubdirectory generic (<T extends PullRequest>), updated types/imports for stronger typing.
Pull request lookup API
apps/api/src/app/pull-requests/services/pull-request.service.ts, apps/api/src/app/pull-requests/services/pull-request.types.ts, apps/api/src/app/pull-requests/resolvers/queries/code-review-pull-requests.query.ts
Changed findPullRequestById to accept a FindPullRequestByIdArgs object with optional include; added FindPullRequestByIdArgs type and updated call sites.
PR tracking metrics, transformer & schema
apps/api/src/app/github/services/github-pull-request-tracking.service.ts, apps/api/src/app/pull-requests/resolvers/pull-requests.schema.ts, apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts
Extended getTimeToMerge signature, added getTimeToDeploy; refactored transformer to accept { tracking, pullRequest } and compute timeToCode, firstDeployedAt, timeToDeploy; GraphQL schema adds those fields.
Merge-commit propagation & workers
apps/api/src/app/github/services/github-pull-request.service.ts, apps/api/src/app/github/workers/github-sync-pull-request.worker.ts, apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts
Introduced webhookMergeCommitSha (prefer webhook, fallback to HTTP fetch); worker payload changed to pullRequestId and now fetches PR (including tracking) before deployment flow.
Frontend GraphQL types & documents
packages/graphql-types/api.ts, packages/graphql-types/frontend/gql.ts, packages/graphql-types/frontend/graphql.ts, apps/web/src/api/pull-request.api.ts, apps/web/src/api/deployments.api.ts, apps/web/src/api/teams.api.ts, apps/web/src/api/work-log.api.ts
Added timeToCode, timeToDeploy, firstDeployedAt to GraphQL types/documents and regenerated frontend documents/overloads so queries include new fields.
Frontend UI: timeline & badges
apps/web/src/components/card-pull-request/timeline-pull-request.tsx, apps/web/src/components/card-pull-request/use-badges.tsx
Timeline shows new "Coding" phase and "Deployed" state; color mapping updated; deploy badge added with thresholds based on timeToDeploy and firstDeployedAt.
Logger, resync guard, small UX
apps/api/src/lib/logger.ts, apps/api/src/app/sync-batch/resolvers/mutations/schedule-sync-batch.mutation.ts, apps/web/src/app/settings/workspace/resync/page.tsx
Logger sanitizes workspace/pullRequest subfields; 24-hour resync throttle gated by isLive; minor button label change (Cancel→Close).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • sweetrdev
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title references the main feature (PR deployed tracking) but includes a vague phrase (many misc changes) that obscures the breadth of changes and doesn't reflect the multiple significant additions like timeToDeploy, GraphQL expansion, and frontend timeline updates. Consider using a more specific title that captures the main feature focus, such as 'feat: add deployment tracking and timeToDeploy metrics' or break into multiple PRs if the scope is too broad.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description comprehensively explains the changes, including new deployment tracking functionality, worker refactoring, GitHub service updates, transformer improvements, frontend enhancements, and miscellaneous updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pr-tracking-deployment-fields

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 18, 2026

Greptile Summary

This PR adds deployment tracking for pull requests (firstDeployedAt, timeToDeploy), exposes the existing timeToCode metric through the GraphQL API and frontend timeline, and includes several miscellaneous improvements.

  • PR deployment tracking: When a deployment is created (either from PR merge or auto-linking), the PR's tracking record is updated with firstDeployedAt and timeToDeploy. The logic correctly preserves the earliest deployment date.
  • timeToCode exposed: The timeToCode field (already computed in the backend) is now surfaced in the GraphQL schema and displayed as a new "Coding" step in the frontend PR timeline.
  • Merge commit SHA fallback: When the webhook doesn't provide merge_commit_sha, the system now fetches it from the GitHub REST API as a fallback, fixing cases where merged PRs lacked this data during batch syncs.
  • Worker refactor: The deployment-triggered-by-merge worker now receives pullRequestId instead of the full PullRequest object, fetching fresh data with tracking included.
  • Logger sanitization: Adds field-level sanitization for workspace and pullRequest objects in logs, but mutates the caller's object and uses incorrect field names for PullRequest (uses name/description instead of title/number).
  • Sync batch throttle bypass: The 24-hour resync throttle is now skipped in non-live (dev/staging) environments.
  • getTimeToMerge improvement: Falls back to firstReadyAt when no approval date is available, providing more accurate time-to-merge calculations.

Confidence Score: 3/5

  • The core deployment tracking logic is sound, but the logger changes have two bugs that will cause silent data loss in logs and unexpected mutation of caller objects.
  • The deployment tracking feature is well-structured with proper guards and idempotent behavior. However, the logger sanitization introduces a mutation side-effect bug (modifying caller's objects) and uses incorrect field names for PullRequest, meaning PR titles will be silently dropped from all info-level logs. These issues affect observability across the entire application.
  • apps/api/src/lib/logger.ts needs attention — it mutates caller objects and uses wrong field names for PullRequest sanitization.

Important Files Changed

Filename Overview
apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Adds updatePullRequestDeploymentTracking function and renames findPullRequestsByCommitHashes to findMergedPullRequestsByCommitHashes with merged-only filter. Contains a typo in error message ("Depoloyed").
apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts Refactored to pass pullRequestId instead of full PullRequest object, fetching fresh data with tracking included.
apps/api/src/app/github/services/github-pull-request-tracking.service.ts Adds getTimeToDeploy function and updates getTimeToMerge to accept firstReadyAt as a fallback date.
apps/api/src/app/github/services/github-pull-request.service.ts Adds fetchPullRequestMergeCommitSha REST API fallback for when webhook doesn't provide merge commit SHA. Passes firstReadyAt to getTimeToMerge.
apps/api/src/lib/logger.ts Adds log sanitization for workspace and pullRequest fields, but mutates the caller's object and uses incorrect field names for PullRequest.
apps/api/src/app/pull-requests/services/pull-request.service.ts Refactors findPullRequestById to accept an options object with optional include parameter for Prisma relations.
apps/web/src/components/card-pull-request/timeline-pull-request.tsx Adds "Coding" timeline step with timeToCode metric and info tooltip to the PR timeline visualization.

Sequence Diagram

sequenceDiagram
    participant Webhook as GitHub Webhook
    participant SyncWorker as SyncPR Worker
    participant PRService as PR Service
    participant GitHub as GitHub API
    participant DeployWorker as Deploy Worker
    participant DeployService as Deploy Service
    participant DB as Database

    Webhook->>SyncWorker: PR merged event
    SyncWorker->>PRService: syncPullRequest(webhookMergeCommitSha)
    alt webhookMergeCommitSha missing
        PRService->>GitHub: REST API: pulls.get()
        GitHub-->>PRService: merge_commit_sha
    end
    PRService->>DB: upsert PullRequest
    PRService->>DB: upsert PullRequestTracking (timeToCode, timeToMerge)
    PRService-->>SyncWorker: pullRequest

    SyncWorker->>DeployWorker: enqueue(pullRequestId)
    DeployWorker->>DB: findPullRequestById(include: tracking)
    DeployWorker->>DeployService: createDeploymentFromPullRequestMerge
    DeployService->>DB: upsert Deployment
    DeployService->>DB: link PR to Deployment
    DeployService->>DB: update PullRequestTracking(firstDeployedAt, timeToDeploy)
Loading

Last reviewed commit: f94e713

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/lib/logger.ts Outdated
Comment thread apps/api/src/lib/logger.ts
Comment thread apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (5)
apps/api/src/lib/logger.ts (1)

37-37: Redundant || {}cleanObj is already guaranteed non-nullish

cleanObj is assigned obj || {} on line 29, so it is never falsy. The || {} guard on line 37 is dead code.

🧹 Nit
-    pinoLogger.info(cleanObj || {}, msg);
+    pinoLogger.info(cleanObj, msg);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/logger.ts` at line 37, The call to pinoLogger.info uses a
redundant fallback (pinoLogger.info(cleanObj || {}, msg)) even though cleanObj
is already initialized as obj || {} earlier; remove the dead guard and pass
cleanObj directly (i.e., change to pinoLogger.info(cleanObj, msg)) so the log
call relies on the guaranteed non-nullish cleanObj variable in logger.ts.
apps/api/src/app/github/services/github-pull-request-tracking.service.ts (1)

99-104: Consider adding a guard for deployedAt before mergedAt.

If deployedAt is earlier than mergedAt (e.g., clock skew, data inconsistency), differenceInBusinessMilliseconds would return a negative value. A simple guard or Math.max(0, ...) would make this more defensive.

🛡️ Optional defensive guard
 export const getTimeToDeploy = (mergedAt: Date, deployedAt: Date) => {
+  if (deployedAt < mergedAt) return 0;
   return differenceInBusinessMilliseconds(mergedAt, deployedAt);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/github/services/github-pull-request-tracking.service.ts`
around lines 99 - 104, The getTimeToDeploy function can return negative values
if deployedAt is earlier than mergedAt; update getTimeToDeploy (which calls
differenceInBusinessMilliseconds) to guard against negative results by ensuring
you compute max(0, differenceInBusinessMilliseconds(mergedAt, deployedAt)) or by
swapping/validating the dates before calling differenceInBusinessMilliseconds so
the function always returns zero or positive milliseconds.
apps/api/src/app/pull-requests/services/pull-request.service.ts (1)

123-137: Return type doesn't reflect included relations.

Since findPullRequestById always returns PullRequest | null regardless of the include parameter, callers that pass include: { tracking: true } won't get type-safe access to tracking on the result without a cast. Consider using a generic signature to propagate the include shape into the return type.

This isn't blocking since callers can cast or assert, but it reduces the benefit of the typed include parameter.

♻️ Example with generic return type
-export const findPullRequestById = async ({
-  workspaceId,
-  pullRequestId,
-  include = {},
-}: FindPullRequestByIdArgs) => {
+export const findPullRequestById = async <
+  T extends Prisma.PullRequestInclude = {},
+>({
+  workspaceId,
+  pullRequestId,
+  include = {} as T,
+}: Omit<FindPullRequestByIdArgs, 'include'> & { include?: T }) => {
   return getPrisma(workspaceId).pullRequest.findUnique({
     where: {
       id: pullRequestId,
       workspaceId,
     },
     include: {
       ...include,
     },
-  });
+  }) as Promise<Prisma.PullRequestGetPayload<{ include: T }> | null>;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/pull-requests/services/pull-request.service.ts` around lines
123 - 137, The function findPullRequestById always types its result as
PullRequest | null which ignores the shape of the include parameter; change it
to a generic signature so the include shape is propagated to the return type
(use Prisma's helper types). For example, make findPullRequestById generic (e.g.
<T extends Prisma.PullRequestInclude | null = null>), accept include?: T in
FindPullRequestByIdArgs, and set the return type to
Promise<Prisma.PullRequestGetPayload<{ include: T }> | null>; keep the existing
implementation body (getPrisma(...).pullRequest.findUnique(...)) but ensure the
include variable is typed as T so callers receive type-safe included relations
like tracking.
apps/api/src/app/deployment/services/deployment-create-from-merge.service.ts (1)

56-80: No transaction wraps deployment creation, linking, and tracking update.

If updatePullRequestDeploymentTracking fails (Line 76), the deployment and PR link are already persisted. Since upsertDeployment is idempotent, retries should be safe, but the caller will see a failure despite a partially successful operation. Consider whether this is acceptable or if a transaction would be warranted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/deployment/services/deployment-create-from-merge.service.ts`
around lines 56 - 80, The current sequence calls upsertDeployment,
linkPullRequestsToDeployment, and updatePullRequestDeploymentTracking separately
which can leave persisted state if the final call fails; wrap these three
operations in a single database transaction (or use the repository/ORM
transaction helper) so that upsertDeployment, linkPullRequestsToDeployment, and
updatePullRequestDeploymentTracking are executed atomically and rolled back on
error, or alternatively implement a compensating rollback when
updatePullRequestDeploymentTracking fails; locate these calls (upsertDeployment,
linkPullRequestsToDeployment, updatePullRequestDeploymentTracking) in
deployment-create-from-merge.service.ts and move them into a transactional block
that commits only after all succeed.
packages/graphql-types/frontend/graphql.ts (1)

1797-1797: timeToCode included in PullRequestsQuery but not in other PR-fetching queries.

The timeToCode field is now part of the PullRequestsQuery tracking selection. Note that other queries fetching PR tracking data (DeploymentQuery on Line 1577, TeamPullRequestsInProgressQuery on Line 1862, TeamWorkLogQuery on Line 1900) do not include timeToCode. If this is intentional (e.g., only needed in specific views for now), this is fine — just flagging for awareness in case it should be consistent across all PR tracking selections.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/graphql-types/frontend/graphql.ts` at line 1797, PullRequestsQuery
includes the tracking.timeToCode field while other PR-tracking queries
(DeploymentQuery, TeamPullRequestsInProgressQuery, TeamWorkLogQuery) do not;
decide whether timeToCode should be consistently returned and, if so, add
timeToCode to the tracking selection in those queries (or if intentional, leave
PullRequestsQuery as-is and add a comment near PullRequestsQuery explaining why
timeToCode is only selected there). Update the GraphQL query definitions for
DeploymentQuery, TeamPullRequestsInProgressQuery, and TeamWorkLogQuery (or add
the explanatory comment in PullRequestsQuery) to reflect the chosen consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts`:
- Around line 167-213: The DataIntegrityException message in
updatePullRequestDeploymentTracking contains a typo ("Depoloyed"); update the
thrown DataIntegrityException call to correct the text to "Deployed Pull Request
is not merged" (keep the surrounding context and the extra: { pullRequest }
payload untouched) so the error message reads correctly when
DataIntegrityException is raised.
- Around line 200-212: The update call to
getPrisma(...).pullRequestTracking.update assumes a tracking row exists and can
raise Prisma P2025 when pullRequest.tracking is null; change this to either (a)
guard before calling update and create the tracking row if missing, or (b)
replace the update with pullRequestTracking.upsert using pullRequest.id and
workspaceId as the unique key so it will create or update atomically; ensure
this prevents unhandled errors inside the Promise.all batch that processes
deployments.

In `@apps/api/src/app/deployment/services/deployment-pr-linking.types.ts`:
- Around line 38-43: The UpdatePullRequestDeploymentTrackingArgs type allows
pullRequest.tracking to be null, but updatePullRequestDeploymentTracking
currently calls prisma.pullRequestTracking.update() unguarded; change the
service to either (a) perform an early null check/return when
pullRequest.tracking is null before calling prisma.pullRequestTracking.update(),
or (b) replace the update call with prisma.pullRequestTracking.upsert(...) using
pullRequest.tracking?.id (or pullRequest.id) as the key and providing both
create and update payloads so a missing tracking record is created; update the
service code around prisma.pullRequestTracking.update() accordingly to ensure no
runtime throw when tracking is null.

In
`@apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts`:
- Around line 34-46: The pullRequest returned from findPullRequestById is
inferred with an optional include so TypeScript can't guarantee the tracking
field for the downstream call to createDeploymentFromPullRequestMerge; add a
local type assertion when passing pullRequest (assert as PullRequest & {
tracking: PullRequestTracking | null }) so the callsite satisfies the expected
shape, or alternatively change findPullRequestById to a generic overload that
narrows its return type based on the include parameter; update the call that
supplies pullRequest to createDeploymentFromPullRequestMerge to use the
asserted/narrowed type and ensure imports for PullRequest and
PullRequestTracking are present.

In `@apps/api/src/app/github/services/github-pull-request.service.ts`:
- Around line 195-211: fetchPullRequestMergeCommitSha currently calls
octokit.rest.pulls.get without error handling and can throw; wrap the REST call
in a try/catch inside fetchPullRequestMergeCommitSha, returning undefined on any
failure (matching getFirstCommit's behavior) and optionally log the error (e.g.,
console.error or the module logger) with context including installationId and
gitPrData.number; ensure the function still returns
response.data.merge_commit_sha || undefined on success.

In `@apps/api/src/lib/logger.ts`:
- Around line 29-35: The code currently mutates the caller's object because
const cleanObj = obj || {} only copies the reference; change cleanObj to be a
shallow copy (e.g., const cleanObj = obj ? { ...obj } : {}) so subsequent
cleanObj[key] = pick(...) does not modify obj, and ensure the sanitized object
(cleanObj) is passed to Sentry.addBreadcrumb (instead of obj); update usages
around cleanObj, loggableFields and pick to operate on the new shallow copy so
the original obj remains unchanged.
- Line 24: The loggable fields array for pullRequest is wrong — replace the
current array ["id", "name", "description", "createdAt", "updatedAt"] with the
correct PullRequest fields such as ["id", "title", "createdAt", "updatedAt"] (or
add other valid fields like "number" or "state" if desired) so the logger picks
up the PR title and valid attributes; update the pullRequest array definition in
logger.ts accordingly (refer to the pullRequest variable/constant where the
array is declared).
- Around line 31-34: The TypeScript error comes from indexing cleanObj (typed as
object) and loggableFields with a general string; update the usage in the loop
inside apps/api/src/lib/logger.ts (symbols: cleanObj, loggableFields, pick) so
cleanObj is treated as Record<string, unknown> (either change the parameter type
obj?: object to obj?: Record<string, unknown> or cast cleanObj to that type
before indexing) and guard the access to loggableFields with a proper key check
(use `if (key in loggableFields)` and index as `loggableFields[key as keyof
typeof loggableFields]`) before calling pick, ensuring the types align for
pick's arguments.

In `@apps/web/src/components/card-pull-request/timeline-pull-request.tsx`:
- Around line 132-134: The Text node rendering the coding duration uses
{timeToCode && <>{humanizeDuration(timeToCode)}</>} which omits the "in " prefix
and skips rendering when timeToCode is 0; update this to render consistently
with other entries by checking for a defined numeric value (e.g., timeToCode !==
null && timeToCode !== undefined or typeof timeToCode === "number") and render
the prefix string "in " followed by humanizeDuration(timeToCode) (referencing
the timeToCode variable and humanizeDuration function in the Text component) so
zero durations still display "in 0s" while maintaining consistency with other
timeline entries.

---

Nitpick comments:
In
`@apps/api/src/app/deployment/services/deployment-create-from-merge.service.ts`:
- Around line 56-80: The current sequence calls upsertDeployment,
linkPullRequestsToDeployment, and updatePullRequestDeploymentTracking separately
which can leave persisted state if the final call fails; wrap these three
operations in a single database transaction (or use the repository/ORM
transaction helper) so that upsertDeployment, linkPullRequestsToDeployment, and
updatePullRequestDeploymentTracking are executed atomically and rolled back on
error, or alternatively implement a compensating rollback when
updatePullRequestDeploymentTracking fails; locate these calls (upsertDeployment,
linkPullRequestsToDeployment, updatePullRequestDeploymentTracking) in
deployment-create-from-merge.service.ts and move them into a transactional block
that commits only after all succeed.

In `@apps/api/src/app/github/services/github-pull-request-tracking.service.ts`:
- Around line 99-104: The getTimeToDeploy function can return negative values if
deployedAt is earlier than mergedAt; update getTimeToDeploy (which calls
differenceInBusinessMilliseconds) to guard against negative results by ensuring
you compute max(0, differenceInBusinessMilliseconds(mergedAt, deployedAt)) or by
swapping/validating the dates before calling differenceInBusinessMilliseconds so
the function always returns zero or positive milliseconds.

In `@apps/api/src/app/pull-requests/services/pull-request.service.ts`:
- Around line 123-137: The function findPullRequestById always types its result
as PullRequest | null which ignores the shape of the include parameter; change
it to a generic signature so the include shape is propagated to the return type
(use Prisma's helper types). For example, make findPullRequestById generic (e.g.
<T extends Prisma.PullRequestInclude | null = null>), accept include?: T in
FindPullRequestByIdArgs, and set the return type to
Promise<Prisma.PullRequestGetPayload<{ include: T }> | null>; keep the existing
implementation body (getPrisma(...).pullRequest.findUnique(...)) but ensure the
include variable is typed as T so callers receive type-safe included relations
like tracking.

In `@apps/api/src/lib/logger.ts`:
- Line 37: The call to pinoLogger.info uses a redundant fallback
(pinoLogger.info(cleanObj || {}, msg)) even though cleanObj is already
initialized as obj || {} earlier; remove the dead guard and pass cleanObj
directly (i.e., change to pinoLogger.info(cleanObj, msg)) so the log call relies
on the guaranteed non-nullish cleanObj variable in logger.ts.

In `@packages/graphql-types/frontend/graphql.ts`:
- Line 1797: PullRequestsQuery includes the tracking.timeToCode field while
other PR-tracking queries (DeploymentQuery, TeamPullRequestsInProgressQuery,
TeamWorkLogQuery) do not; decide whether timeToCode should be consistently
returned and, if so, add timeToCode to the tracking selection in those queries
(or if intentional, leave PullRequestsQuery as-is and add a comment near
PullRequestsQuery explaining why timeToCode is only selected there). Update the
GraphQL query definitions for DeploymentQuery, TeamPullRequestsInProgressQuery,
and TeamWorkLogQuery (or add the explanatory comment in PullRequestsQuery) to
reflect the chosen consistency.

Comment thread apps/api/src/app/deployment/services/deployment-pr-linking.types.ts
Comment thread apps/api/src/app/github/services/github-pull-request.service.ts
Comment thread apps/api/src/lib/logger.ts Outdated
Comment thread apps/api/src/lib/logger.ts Outdated
Comment on lines +31 to +34
for (const key of Object.keys(cleanObj)) {
if (loggableFields[key]) {
cleanObj[key] = pick(cleanObj[key], loggableFields[key]);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check tsconfig for strict/noImplicitAny settings
fd 'tsconfig.*\.json' apps/api --exec cat {}

Repository: sweetr-dev/sweetr.dev

Length of output: 958


🏁 Script executed:

# Read the logger.ts file to see the complete context
cat -n apps/api/src/lib/logger.ts | head -60

Repository: sweetr-dev/sweetr.dev

Length of output: 1911


TypeScript type errors: object cannot be indexed by string

cleanObj is typed as object (from obj?: object), which does not allow bracket-notation access with a string key. Additionally, loggableFields is typed as { workspace: string[]; pullRequest: string[] } — accessing it with a bare string key is a type error since the key is not restricted to the literal union "workspace" | "pullRequest".

The proposed fix (casting cleanObj to Record<string, unknown> and using key in loggableFields + key as keyof typeof loggableFields) resolves both issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/logger.ts` around lines 31 - 34, The TypeScript error comes
from indexing cleanObj (typed as object) and loggableFields with a general
string; update the usage in the loop inside apps/api/src/lib/logger.ts (symbols:
cleanObj, loggableFields, pick) so cleanObj is treated as Record<string,
unknown> (either change the parameter type obj?: object to obj?: Record<string,
unknown> or cast cleanObj to that type before indexing) and guard the access to
loggableFields with a proper key check (use `if (key in loggableFields)` and
index as `loggableFields[key as keyof typeof loggableFields]`) before calling
pick, ensuring the types align for pick's arguments.

Comment thread apps/web/src/components/card-pull-request/timeline-pull-request.tsx
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 23 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/lib/logger.ts">

<violation number="1" location="apps/api/src/lib/logger.ts:24">
P2: Incorrect field names for `pullRequest` in loggable fields. The PullRequest model uses `title` (not `name`) and doesn't have a `description` field. This will silently drop the PR title from every info log since these fields are undefined on the model.</violation>

<violation number="2" location="apps/api/src/lib/logger.ts:29">
P2: Avoid mutating the caller’s log object; clone it before sanitizing so logging doesn’t change application state.</violation>
</file>

<file name="apps/api/src/app/deployment/services/deployment-pr-linking.service.ts">

<violation number="1" location="apps/api/src/app/deployment/services/deployment-pr-linking.service.ts:174">
P3: Typo in error message: "Depoloyed" should be "Deployed".</violation>

<violation number="2" location="apps/api/src/app/deployment/services/deployment-pr-linking.service.ts:200">
P2: Guard for missing pullRequest.tracking (or use a safe update) before calling `pullRequestTracking.update`, otherwise PRs without tracking rows will throw and stop deployment linking.</violation>
</file>

<file name="apps/api/src/app/github/services/github-pull-request.service.ts">

<violation number="1" location="apps/api/src/app/github/services/github-pull-request.service.ts:204">
P2: Missing error handling on REST API call. If this GitHub API call fails (rate limit, 404 for deleted repo, network error), the entire `upsertPullRequest` flow will throw and the PR sync will fail. Wrap in try/catch and return `undefined` on error, similar to `getFirstCommit`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/api/src/lib/logger.ts Outdated
Comment thread apps/api/src/lib/logger.ts Outdated
Comment thread apps/api/src/app/github/services/github-pull-request.service.ts Outdated
Comment thread apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Outdated
waltergalvao and others added 3 commits February 18, 2026 02:18
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/lib/logger.ts (1)

42-42: ⚠️ Potential issue | 🟡 Minor

Sentry breadcrumb still receives the unsanitized obj

The pick-based redaction applied to cleanObj (lines 31–35) is bypassed here — Sentry receives the raw original object with all fields intact. The prior commit addressed the mutation issue but left the breadcrumb data unchanged.

🛡️ Proposed fix
     addBreadcrumb({
       message: msg,
       category: "log",
       level: "info",
-      data: obj,
+      data: cleanObj,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/logger.ts` at line 42, The Sentry breadcrumb is currently
being sent the unsanitized original object (data: obj) and thus bypasses the
redaction done earlier with pick/cleanObj; update the breadcrumb to use the
sanitized/derived value (e.g., data: cleanObj or JSON.stringify(cleanObj) if
serialization is required) and ensure you do not mutate obj—refer to the
existing cleanObj and pick logic and replace the usage of obj in the breadcrumb
creation (the data: field) with the sanitized cleanObj.
🧹 Nitpick comments (1)
apps/api/src/lib/logger.ts (1)

37-37: cleanObj || {} is always the truthy branch — the fallback is dead code

cleanObj is initialised unconditionally to a spread object literal on line 29, so it is never falsy.

♻️ Proposed simplification
-    pinoLogger.info(cleanObj || {}, msg);
+    pinoLogger.info(cleanObj, msg);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/logger.ts` at line 37, The expression uses a dead fallback:
remove the redundant "|| {}" and pass the actual cleanObj directly to
pinoLogger.info (update the call site pinoLogger.info(cleanObj || {}, msg) to
pinoLogger.info(cleanObj, msg)); if the original intent was to allow undefined,
instead make cleanObj possibly undefined at its initializer and keep the
single-argument call consistent. Target the pinoLogger.info call and the
cleanObj variable in this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/api/src/lib/logger.ts`:
- Line 42: The Sentry breadcrumb is currently being sent the unsanitized
original object (data: obj) and thus bypasses the redaction done earlier with
pick/cleanObj; update the breadcrumb to use the sanitized/derived value (e.g.,
data: cleanObj or JSON.stringify(cleanObj) if serialization is required) and
ensure you do not mutate obj—refer to the existing cleanObj and pick logic and
replace the usage of obj in the breadcrumb creation (the data: field) with the
sanitized cleanObj.

---

Duplicate comments:
In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts`:
- Around line 200-212: The update call to
getPrisma(workspaceId).pullRequestTracking.update can throw Prisma P2025 if
pullRequest.tracking is null; replace the update with an upsert on
pullRequestTracking (use pullRequestId and workspaceId in the where clause, set
the same fields in data, and provide a create object with pullRequestId,
workspaceId, firstDeployedAt and timeToDeploy) so missing tracking rows are
created atomically and the Promise.all won't fail due to a missing row; locate
the call in deployment-pr-linking.service.ts (the block referencing
pullRequest.tracking and getTimeToDeploy) and swap update -> upsert and populate
create and update payloads accordingly.

In `@apps/api/src/lib/logger.ts`:
- Around line 31-34: The loop indexing causes TypeScript errors because cleanObj
is typed as {} and loggableFields is a narrow union; update the loop to use a
runtime guard "if (key in loggableFields)" and narrow/cast types before
indexing: treat cleanObj as Record<string, unknown> (or cast cleanObj[key] as
any/unknown) and cast loggableFields[key] to the appropriate type when calling
pick so TypeScript accepts cleanObj[key] and loggableFields[key]; adjust the
for-loop that iterates Object.keys(cleanObj) and references cleanObj,
loggableFields, and pick accordingly.

---

Nitpick comments:
In `@apps/api/src/lib/logger.ts`:
- Line 37: The expression uses a dead fallback: remove the redundant "|| {}" and
pass the actual cleanObj directly to pinoLogger.info (update the call site
pinoLogger.info(cleanObj || {}, msg) to pinoLogger.info(cleanObj, msg)); if the
original intent was to allow undefined, instead make cleanObj possibly undefined
at its initializer and keep the single-argument call consistent. Target the
pinoLogger.info call and the cleanObj variable in this file.

@sweetr-dev sweetr-dev Bot added huge Huge PR - High risk of reviewer fatigue and removed large Large PR - Consider splitting up into smaller PRs to reduce risk and review time labels Feb 18, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts">

<violation number="1" location="apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts:76">
P2: timeToDeploy should be null until the PR is merged; falling back to createdAt computes a misleading duration for unmerged PRs. Use mergedAt as the start time so calculateTimeForEvent returns null when it hasn’t merged yet.</violation>
</file>

<file name="apps/web/src/components/card-pull-request/use-badges.tsx">

<violation number="1" location="apps/web/src/components/card-pull-request/use-badges.tsx:183">
P2: The early return requires `isDeployed` to be true, which makes the “Not deployed”/“Stuck on deploy” label logic unreachable. This prevents the deploy badge from showing warning/error states for merged-but-not-deployed PRs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/web/src/components/card-pull-request/use-badges.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts (1)

40-87: ⚠️ Potential issue | 🟠 Major

Compute timeToDeploy only after merge to avoid misleading durations.

When mergedAt is null, the current fallback uses createdAt, which inflates “pending deploy” for unmerged PRs. Use mergedAt only so open PRs don’t get a deploy timer.

🛠️ Fix timeToDeploy start point
     timeToDeploy: calculateTimeForEvent({
-      from: pullRequest.mergedAt || pullRequest.createdAt,
+      from: pullRequest.mergedAt,
       duration: tracking.timeToDeploy,
       pullRequest,
       uselessAfterMerge: false,
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`
around lines 40 - 87, The timeToDeploy currently falls back to
pullRequest.createdAt when mergedAt is null, inflating deploy time for open PRs;
change the timeToDeploy field so it only calls calculateTimeForEvent when
pullRequest.mergedAt is present (e.g., timeToDeploy: pullRequest.mergedAt ?
calculateTimeForEvent({ from: pullRequest.mergedAt, duration:
tracking.timeToDeploy, pullRequest, uselessAfterMerge: false }) : null) so
open/unmerged PRs return null/absent deploy timing instead of a misleading
duration.
🧹 Nitpick comments (3)
apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts (1)

24-37: Return explicit nulls for new fields when tracking is absent.

Keeps the output shape stable and avoids undefined leaking to clients.

♻️ Suggested tweak
     return {
       size: PullRequestSize.MEDIUM,
       changedFilesCount: 0,
       linesAddedCount: 0,
       linesDeletedCount: 0,
       firstApprovalAt: null,
       firstReviewAt: null,
       timeToCode: null,
+      firstDeployedAt: null,
+      timeToDeploy: null,
       timeToFirstApproval: null,
       timeToFirstReview: null,
       timeToMerge: null,
       cycleTime: null,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`
around lines 24 - 37, When tracking is falsy in
pull-request-tracking.transformer.ts, the fallback object currently omits some
new fields which can lead to undefined values leaking to clients; update the
early return where it checks if (!tracking) to include explicit nulls for every
new nullable field (e.g., firstApprovalAt, firstReviewAt, timeToCode,
timeToFirstApproval, timeToFirstReview, timeToMerge, cycleTime) while keeping
existing defaults (size: PullRequestSize.MEDIUM, changedFilesCount: 0,
linesAddedCount: 0, linesDeletedCount: 0) so the output shape from the
transformer is stable and all fields are defined.
apps/api/src/app/deployment/services/deployment-pr-linking.service.ts (2)

145-152: Defensive mergedAt check is redundant but acceptable as a safety net.

findMergedPullRequestsByCommitHashes (line 291) already filters with mergedAt: { not: null }, so filteredPullRequests should never contain un-merged PRs. This check is purely defensive. If you keep it, consider downgrading from a thrown exception to a warning log with early return, since throwing here aborts the entire linking flow for what would be a database-level inconsistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts` around
lines 145 - 152, The current defensive check in
handleDeploymentPullRequestAutoLinking that throws DataIntegrityException when
filteredPullRequests contains a PR with a falsy mergedAt should be changed to
log a warning and early return instead of throwing; locate the
filteredPullRequests usage and the call site tied to
findMergedPullRequestsByCommitHashes (which already filters mergedAt: { not:
null }) and replace the throw with a processLogger.warn or equivalent warning
that includes the filteredPullRequests and the fact that mergedAt was
unexpectedly missing, then return early from
handleDeploymentPullRequestAutoLinking to avoid aborting the whole linking flow.

160-177: Promise.allSettled results are discarded — failures are silently swallowed.

The DataIntegrityException thrown at line 163 and any Prisma errors from updatePullRequestDeploymentTracking will be captured as rejected settlements, but since the return value of Promise.allSettled is never inspected, these failures are completely silent. This makes debugging deployment tracking issues difficult.

Consider either logging rejected results or switching to Promise.all if you want failures to propagate.

♻️ Option: Log rejected settlements
- await Promise.allSettled(
+ const results = await Promise.allSettled(
    filteredPullRequests.map(async (pr) => {
      if (!pr.tracking) {
        throw new DataIntegrityException(
          "[updatePullRequestDeploymentTracking] Pull Request tracking not found",
          {
            extra: { pr },
          }
        );
      }

      return updatePullRequestDeploymentTracking({
        pullRequest: pr as PullRequest & { tracking: PullRequestTracking },
        deployment,
        workspaceId,
      });
    })
  );
+
+ const rejected = results.filter(
+   (r): r is PromiseRejectedResult => r.status === "rejected"
+ );
+
+ if (rejected.length > 0) {
+   logger.error(
+     "handleDeploymentPullRequestAutoLinking: Some PR tracking updates failed",
+     {
+       deploymentId,
+       workspaceId,
+       errors: rejected.map((r) => r.reason),
+     }
+   );
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts` around
lines 160 - 177, The Promise.allSettled call over filteredPullRequests currently
discards settlement results so thrown DataIntegrityException and any errors from
updatePullRequestDeploymentTracking are silently ignored; change this by either
replacing Promise.allSettled with Promise.all to let rejections propagate (so
the caller sees failures) or keep allSettled but capture its result array,
iterate over the settlements and log or rethrow rejected entries (including the
reason and the associated pr) before returning; update the block referencing
Promise.allSettled, filteredPullRequests, updatePullRequestDeploymentTracking,
and DataIntegrityException accordingly to ensure errors are surfaced or logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts`:
- Around line 27-41: The code currently checks workspaceId but not
pullRequestId, so add a defensive guard for pullRequestId (from job.data) before
calling findPullRequestById: if pullRequestId is missing or falsy, throw a
ResourceNotFoundException("Pull request not found", { extra: { data: job.data }
}) to mirror the workspaceId check and avoid passing an undefined pullRequestId
into findPullRequestById; update any surrounding comments and ensure the thrown
exception type and extra payload match the existing workspaceId guard.

In
`@apps/api/src/app/pull-requests/resolvers/queries/pull-requests-tracking.query.ts`:
- Around line 26-33: transformPullRequestTracking is being fed Invalid Date
because parseISO is called on Prisma Date objects; change the construction of
the pullRequest object so you only call parseISO when the value is a string and
otherwise pass the Date through (or convert to ISO string if
transformPullRequestTracking expects strings). Concretely, update the fields
createdAt and mergedAt in the object passed to transformPullRequestTracking to:
if typeof pullRequest.createdAt === 'string' then
parseISO(pullRequest.createdAt) else pullRequest.createdAt (and same conditional
for mergedAt), so parseISO is never given a Date instance.

In `@apps/web/src/components/card-pull-request/use-badges.tsx`:
- Line 183: getLabel() currently has unreachable branches because the guard if
(!isNumber(timeToDeploy) || !isDeployed) return null forces isDeployed===true
for the rest of the function, so the "Stuck on deploy" and "Not deployed"
branches can never run; fix by either (A) simplifying getLabel() to only return
the "Deployed" label and remove the dead "Stuck on deploy"/"Not deployed"
branches if undeployed states are out of scope, or (B) support undeployed states
by changing the guard and/or inputs: stop gating on isDeployed with
timeToDeploy, introduce or use a separate metric (e.g., timeSinceMerge or
mergeTimestamp) and branch on isDeployed separately inside getLabel() so you can
return "Not deployed" (when !isDeployed and threshold not exceeded) or "Stuck on
deploy" (when !isDeployed and elapsed > threshold), updating any callers that
rely on timeToDeploy/isDeployed accordingly.

---

Outside diff comments:
In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 40-87: The timeToDeploy currently falls back to
pullRequest.createdAt when mergedAt is null, inflating deploy time for open PRs;
change the timeToDeploy field so it only calls calculateTimeForEvent when
pullRequest.mergedAt is present (e.g., timeToDeploy: pullRequest.mergedAt ?
calculateTimeForEvent({ from: pullRequest.mergedAt, duration:
tracking.timeToDeploy, pullRequest, uselessAfterMerge: false }) : null) so
open/unmerged PRs return null/absent deploy timing instead of a misleading
duration.

---

Duplicate comments:
In `@apps/web/src/components/card-pull-request/timeline-pull-request.tsx`:
- Around line 126-152: The "Coding" Timeline.Item currently renders just the
duration value (using timeToCode and humanizeDuration) but needs the same "in
{duration}" prefix used elsewhere; update the JSX inside Timeline.Item (where
timeToCode and humanizeDuration are used) to render the literal "in " before the
duration (e.g., output "in " + humanizeDuration(timeToCode) only when
isNumber(timeToCode) is true) so the display matches other steps.

---

Nitpick comments:
In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts`:
- Around line 145-152: The current defensive check in
handleDeploymentPullRequestAutoLinking that throws DataIntegrityException when
filteredPullRequests contains a PR with a falsy mergedAt should be changed to
log a warning and early return instead of throwing; locate the
filteredPullRequests usage and the call site tied to
findMergedPullRequestsByCommitHashes (which already filters mergedAt: { not:
null }) and replace the throw with a processLogger.warn or equivalent warning
that includes the filteredPullRequests and the fact that mergedAt was
unexpectedly missing, then return early from
handleDeploymentPullRequestAutoLinking to avoid aborting the whole linking flow.
- Around line 160-177: The Promise.allSettled call over filteredPullRequests
currently discards settlement results so thrown DataIntegrityException and any
errors from updatePullRequestDeploymentTracking are silently ignored; change
this by either replacing Promise.allSettled with Promise.all to let rejections
propagate (so the caller sees failures) or keep allSettled but capture its
result array, iterate over the settlements and log or rethrow rejected entries
(including the reason and the associated pr) before returning; update the block
referencing Promise.allSettled, filteredPullRequests,
updatePullRequestDeploymentTracking, and DataIntegrityException accordingly to
ensure errors are surfaced or logged.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 24-37: When tracking is falsy in
pull-request-tracking.transformer.ts, the fallback object currently omits some
new fields which can lead to undefined values leaking to clients; update the
early return where it checks if (!tracking) to include explicit nulls for every
new nullable field (e.g., firstApprovalAt, firstReviewAt, timeToCode,
timeToFirstApproval, timeToFirstReview, timeToMerge, cycleTime) while keeping
existing defaults (size: PullRequestSize.MEDIUM, changedFilesCount: 0,
linesAddedCount: 0, linesDeletedCount: 0) so the output shape from the
transformer is stable and all fields are defined.

Comment thread apps/web/src/components/card-pull-request/use-badges.tsx
@waltergalvao
Copy link
Copy Markdown
Contributor Author

@greptileai

@waltergalvao
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review again

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

@waltergalvao I'll review the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/graphql-types/frontend/gql.ts (1)

37-37: Auto-generated file — changes look consistent with the new tracking fields.

The new timeToCode, timeToDeploy, firstDeployedAt, and cycleTime fields are added uniformly across all relevant query documents and their corresponding overloads.

One observation: the PR tracking field set is duplicated verbatim 4 times within TeamPullRequestsInProgress and 3 times within TeamWorkLog. If the source .graphql files (or inline graphql() calls) used a shared GraphQL fragment, the generated output would be smaller and future field additions would only need a single-point change.

Also applies to: 66-66, 74-74, 79-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/graphql-types/frontend/gql.ts` at line 37, The generated queries
duplicate the pull-request tracking field set (timeToCode, timeToDeploy,
firstDeployedAt, cycleTime and other tracking subfields) across
TeamPullRequestsInProgress and TeamWorkLog; update the source .graphql files or
the inline graphql() calls that define TeamPullRequestsInProgress and
TeamWorkLog to extract that repeated selection into a shared GraphQL fragment
(e.g., PullRequestTrackingFragment) and reference that fragment from each query
so the generated gql.ts (types.DeploymentDocument / the
TeamPullRequestsInProgress and TeamWorkLog query documents) no longer include
verbatim duplicates.
apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts (1)

49-53: Consider separating the "PR not found" and "tracking not found" error cases.

!pullRequest?.tracking conflates two distinct failure modes: the pull request itself not existing, and the PR existing but lacking a tracking record. Both throw the same "Pull request not found" message. While the extra payload includes pullRequest for debugging, splitting these into two explicit checks would make log triage faster.

💡 Suggested separation
-    if (!pullRequest?.tracking) {
-      throw new ResourceNotFoundException("Pull request not found", {
-        extra: { data: job.data, pullRequest },
-      });
-    }
+    if (!pullRequest) {
+      throw new ResourceNotFoundException("Pull request not found", {
+        extra: { data: job.data },
+      });
+    }
+
+    if (!pullRequest.tracking) {
+      throw new ResourceNotFoundException("Pull request tracking not found", {
+        extra: { data: job.data, pullRequestId: pullRequest.id },
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts`
around lines 49 - 53, Split the combined null-check into two explicit checks so
the code distinguishes "pull request not found" from "tracking not found": first
check if pullRequest is falsy and throw ResourceNotFoundException with message
"Pull request not found" and include extra: { data: job.data, pullRequest } (or
omit pullRequest if null), then separately check if pullRequest.tracking is
falsy and throw ResourceNotFoundException with message "Pull request tracking
not found" including extra: { data: job.data, pullRequestId: pullRequest?.id,
tracking: pullRequest?.tracking } to aid debugging; update any surrounding uses
(e.g., in the worker handler where pullRequest is read) to assume tracking
exists only after the second check.
apps/web/src/api/teams.api.ts (1)

169-183: Recommend extracting GraphQL fragments to reduce code duplication.

The full PR field selection—including all 15 fields in the tracking block—is repeated verbatim across drafted, pendingReview, changesRequested, and pendingMerge. This became evident when adding the four new tracking fields (timeToCode, timeToDeploy, firstDeployedAt, cycleTime), which required synchronized changes in all four locations.

GraphQL named fragments would collapse this to a single definition:

♻️ Proposed refactor using named fragments
 graphql(/* GraphQL */ `
+  fragment PrTrackingFields on PullRequestTracking {
+    size
+    changedFilesCount
+    linesAddedCount
+    linesDeletedCount
+    timeToCode
+    timeToFirstReview
+    timeToFirstApproval
+    timeToMerge
+    timeToDeploy
+    firstReviewAt
+    firstApprovalAt
+    firstDeployedAt
+    cycleTime
+  }
+
+  fragment PrInProgressFields on PullRequest {
+    id
+    title
+    gitUrl
+    commentCount
+    changedFilesCount
+    linesAddedCount
+    linesDeletedCount
+    state
+    createdAt
+    mergedAt
+    closedAt
+    tracking {
+      ...PrTrackingFields
+    }
+    author {
+      id
+      avatar
+      handle
+      name
+    }
+    repository {
+      id
+      name
+      fullName
+    }
+  }
+
   query TeamPullRequestsInProgress(
     $workspaceId: SweetID!
     $teamId: SweetID!
   ) {
     workspace(workspaceId: $workspaceId) {
       id
       team(teamId: $teamId) {
         id
         pullRequestsInProgress {
-          drafted {
-            id
-            title
-            ...
-          }
-          pendingReview {
-            id
-            title
-            ...
-          }
-          changesRequested {
-            id
-            title
-            ...
-          }
-          pendingMerge {
-            id
-            title
-            ...
-          }
+          drafted { ...PrInProgressFields }
+          pendingReview { ...PrInProgressFields }
+          changesRequested { ...PrInProgressFields }
+          pendingMerge { ...PrInProgressFields }
         }
       }
     }
   }
 `)

Also applies to: 208-222, 247-261, 286-300

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts`:
- Around line 13-53: No changes required: the previous issues are resolved by
adding the pullRequestId guard in deploymentTriggeredByPullRequestMergeWorker
and switching to fetching the PullRequest via findPullRequestById (with include:
{ tracking: true }) instead of serializing the full PullRequest in job data;
keep the current guards and call site type assertion as-is.

---

Nitpick comments:
In
`@apps/api/src/app/deployment/workers/deployment-triggered-by-pull-request-merge.worker.ts`:
- Around line 49-53: Split the combined null-check into two explicit checks so
the code distinguishes "pull request not found" from "tracking not found": first
check if pullRequest is falsy and throw ResourceNotFoundException with message
"Pull request not found" and include extra: { data: job.data, pullRequest } (or
omit pullRequest if null), then separately check if pullRequest.tracking is
falsy and throw ResourceNotFoundException with message "Pull request tracking
not found" including extra: { data: job.data, pullRequestId: pullRequest?.id,
tracking: pullRequest?.tracking } to aid debugging; update any surrounding uses
(e.g., in the worker handler where pullRequest is read) to assume tracking
exists only after the second check.

In `@packages/graphql-types/frontend/gql.ts`:
- Line 37: The generated queries duplicate the pull-request tracking field set
(timeToCode, timeToDeploy, firstDeployedAt, cycleTime and other tracking
subfields) across TeamPullRequestsInProgress and TeamWorkLog; update the source
.graphql files or the inline graphql() calls that define
TeamPullRequestsInProgress and TeamWorkLog to extract that repeated selection
into a shared GraphQL fragment (e.g., PullRequestTrackingFragment) and reference
that fragment from each query so the generated gql.ts (types.DeploymentDocument
/ the TeamPullRequestsInProgress and TeamWorkLog query documents) no longer
include verbatim duplicates.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

28 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 18, 2026

Additional Comments (1)

apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts
Missing fields in null-tracking fallback

The null-tracking fallback object explicitly sets all other fields to null but omits timeToDeploy and firstDeployedAt. While GraphQL resolves missing nullable fields as null, this is inconsistent with how every other field is handled and could cause issues if a consumer checks hasOwnProperty or iterates over the returned keys.

  if (!tracking) {
    return {
      size: PullRequestSize.MEDIUM,
      changedFilesCount: 0,
      linesAddedCount: 0,
      linesDeletedCount: 0,
      firstApprovalAt: null,
      firstReviewAt: null,
      firstDeployedAt: null,
      timeToCode: null,
      timeToFirstApproval: null,
      timeToFirstReview: null,
      timeToMerge: null,
      timeToDeploy: null,
      cycleTime: null,
    };

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/web/src/app/metrics-and-insights/components/card-dora-metric/dora-card-stat.tsx (1)

106-111: LGTM — arrow direction now correctly reflects higherIsBetter.

All four sign × higherIsBetter combinations produce the expected arrow, and the logic is consistent with getTrendColor().

One optional simplification: since this block is already guarded by change !== 0, change >= 0 is effectively change > 0, so the whole ternary condition collapses to (change > 0) === higherIsBetter.

♻️ Optional simplification
- {(change >= 0 && higherIsBetter) ||
- (change < 0 && !higherIsBetter) ? (
+ {(change > 0) === higherIsBetter ? (
    <IconArrowUpRight size="1rem" stroke={1.5} />
  ) : (
    <IconArrowDownRight size="1rem" stroke={1.5} />
  )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/metrics-and-insights/components/card-dora-metric/dora-card-stat.tsx`
around lines 106 - 111, The ternary rendering for
IconArrowUpRight/IconArrowDownRight can be simplified: since the surrounding
code already ensures change !== 0, replace the condition ((change >= 0 &&
higherIsBetter) || (change < 0 && !higherIsBetter)) with the simpler expression
(change > 0) === higherIsBetter so the arrow direction logic (used with
IconArrowUpRight and IconArrowDownRight) is clearer and remains consistent with
getTrendColor().
apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts (1)

92-101: pullRequest parameter should be required, not optional

Typing pullRequest as optional (pullRequest?: PullRequest) means a future caller could omit it, silently bypassing the CLOSED and uselessAfterMerge guards (lines 107–113). All current call sites always provide it.

♻️ Proposed change
 const calculateTimeForEvent = ({
   from,
   duration,
   pullRequest,
   uselessAfterMerge = false,
 }: {
   from: Date | null;
   duration: bigint | null;
-  pullRequest?: PullRequest;
+  pullRequest: PullRequest;
   uselessAfterMerge?: boolean;
 }) => {

With this change the two optional-chaining accesses (pullRequest?.state) become plain property accesses (pullRequest.state), removing unnecessary guards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`
around lines 92 - 101, Change the calculateTimeForEvent function signature to
require the pullRequest parameter (make pullRequest: PullRequest instead of
pullRequest?: PullRequest), update the internal checks that use optional
chaining (replace pullRequest?.state with pullRequest.state) and ensure the
existing CLOSED/uselessAfterMerge guard logic still runs; this prevents callers
from omitting pullRequest and lets you remove the unnecessary null-safe accesses
while preserving the current behavior around pullRequest.state and
uselessAfterMerge.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts`:
- Around line 161-182: Promise.allSettled on filteredPullRequests currently
discards results so rejections from updatePullRequestDeploymentTracking are
swallowed; change it to collect the settled results, iterate over them, and call
captureException for any result.status === "rejected" (include the rejection
reason and contextual info such as the associated pr, deployment, and
workspaceId). Locate the Promise.allSettled call around
filteredPullRequests.map(...) and the updatePullRequestDeploymentTracking
invoke, then after awaiting Promise.allSettled inspect each settled entry and
captureException with the error (and optionally wrap in a
DataIntegrityException-like message) for any rejected promise so errors are
logged instead of ignored.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 77-82: The timeToDeploy field is incorrectly using
pullRequest.mergedAt || pullRequest.createdAt, causing open PRs to yield their
age; change the call in the timeToDeploy assignment to pass only
pullRequest.mergedAt (remove the "|| pullRequest.createdAt" fallback) so
calculateTimeForEvent can hit its existing !from guard and return null for
un-merged PRs; update the timeToDeploy assignment where calculateTimeForEvent is
invoked to use from: pullRequest.mergedAt and keep uselessAfterMerge: false.
- Around line 56-64: The live calculation for timeToFirstApproval is using
tracking.firstApprovalAt as the earliest "from" candidate which yields a
negative/incorrect elapsed time when duration is null; change the call to
calculateTimeForEvent inside timeToFirstApproval to remove
tracking.firstApprovalAt from the from chain so it uses tracking.firstReadyAt ||
pullRequest.createdAt as the baseline (leave tracking.timeToFirstApproval as the
duration check intact because the precomputed-duration path already covers the
approved case).

---

Nitpick comments:
In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 92-101: Change the calculateTimeForEvent function signature to
require the pullRequest parameter (make pullRequest: PullRequest instead of
pullRequest?: PullRequest), update the internal checks that use optional
chaining (replace pullRequest?.state with pullRequest.state) and ensure the
existing CLOSED/uselessAfterMerge guard logic still runs; this prevents callers
from omitting pullRequest and lets you remove the unnecessary null-safe accesses
while preserving the current behavior around pullRequest.state and
uselessAfterMerge.

In
`@apps/web/src/app/metrics-and-insights/components/card-dora-metric/dora-card-stat.tsx`:
- Around line 106-111: The ternary rendering for
IconArrowUpRight/IconArrowDownRight can be simplified: since the surrounding
code already ensures change !== 0, replace the condition ((change >= 0 &&
higherIsBetter) || (change < 0 && !higherIsBetter)) with the simpler expression
(change > 0) === higherIsBetter so the arrow direction logic (used with
IconArrowUpRight and IconArrowDownRight) is clearer and remains consistent with
getTrendColor().

Comment thread apps/api/src/app/deployment/services/deployment-pr-linking.service.ts Outdated
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

29 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/app/github/services/github-pull-request.service.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 18, 2026

Additional Comments (1)

apps/api/src/app/github/services/github-pull-request.service.ts
timeToCode is always null on first sync

getTimeToCode(startedCodingAt, tracking?.firstReadyAt) reads firstReadyAt from the existing DB tracking record. On the first sync for a PR, tracking is null, so tracking?.firstReadyAt is undefined, and getTimeToCode returns undefined — even though the freshly computed firstReadyAt (line 338-341) is available and being saved in the same upsert.

This means timeToCode only gets a real value after a second sync (e.g. a PR update webhook), since only then does the DB record have firstReadyAt populated.

Consider using the freshly computed firstReadyAt directly:

  const timeToCode = getTimeToCode(startedCodingAt, firstReadyAt ?? tracking?.firstReadyAt);

@waltergalvao
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/app/metrics-and-insights/page.tsx">

<violation number="1" location="apps/web/src/app/metrics-and-insights/page.tsx:133">
P2: The fallback for previousAmount never triggers because the string concatenation happens before the `||`, leading to "undefined deployments" when previousAmount is missing. Apply the fallback before concatenating the suffix.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/web/src/app/metrics-and-insights/page.tsx Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread apps/web/src/app/metrics-and-insights/page.tsx
Copy link
Copy Markdown
Contributor

@sweetrdev sweetrdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address all agents comments

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/app/github/services/github-pull-request.service.ts
Comment thread apps/api/src/lib/logger.ts
waltergalvao and others added 4 commits February 18, 2026 04:11
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes Feb 18, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/app/metrics-and-insights/page.tsx (1)

171-186: ⚠️ Potential issue | 🟡 Minor

Fallback "0 hours" is inconsistent with the abbreviated duration format.

When data is present, getAbbreviatedDuration produces strings like "2d 5h" or "3h 20m", and returns "0s" for zero input. The fallback "0 hours" on lines 178 and 185 doesn't match this style.

Proposed fix
              amount={
                metrics.meanTimeToRecover?.currentAmount
                  ? getAbbreviatedDuration(
                      metrics.meanTimeToRecover?.currentAmount,
                    )
-                  : "0 hours"
+                  : "0s"
              }
              previousAmount={
                metrics.meanTimeToRecover?.previousAmount
                  ? getAbbreviatedDuration(
                      metrics.meanTimeToRecover?.previousAmount,
                    )
-                  : "0 hours"
+                  : "0s"
              }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/metrics-and-insights/page.tsx` around lines 171 - 186, The
MTTR fallback strings are inconsistent with getAbbreviatedDuration output;
replace the literal "0 hours" used in the CardDoraMetric props for amount and
previousAmount with a call to getAbbreviatedDuration(0) (or the function's
zero-value equivalent) so metrics.meanTimeToRecover?.currentAmount ?
getAbbreviatedDuration(...) : getAbbreviatedDuration(0) and likewise for
previousAmount, referencing CardDoraMetric and metrics.meanTimeToRecover to
locate the code.
🧹 Nitpick comments (3)
apps/api/src/lib/logger.ts (1)

49-93: Field sanitization is only applied to logger.info; other levels (warn, error, debug, trace, fatal) pass the raw obj through.

If the intent is to guard sensitive fields uniformly, the same cleanObj loop should be extracted into a shared helper and reused across all levels. As-is, a logger.error("...", { workspace, pullRequest }) call would still expose unredacted fields to both pino and Sentry.

♻️ Proposed refactor — extract sanitizer helper
+const sanitize = (obj?: object): Record<string, unknown> => {
+  const cleanObj: Record<string, unknown> = { ...(obj || {}) };
+  for (const key of Object.keys(cleanObj)) {
+    if (
+      key in loggableFields &&
+      typeof cleanObj[key] === "object" &&
+      cleanObj[key] !== null
+    ) {
+      cleanObj[key] = pick(
+        cleanObj[key] as object,
+        loggableFields[key as keyof typeof loggableFields]
+      );
+    }
+  }
+  return cleanObj;
+};
+
 export const logger = {
   info: (msg: string, obj?: object) => {
-    const cleanObj = { ...(obj || {}) };
-    for (const key of Object.keys(cleanObj)) {
-      if (
-        loggableFields[key] &&
-        typeof cleanObj[key] === "object" &&
-        cleanObj[key] !== null
-      ) {
-        cleanObj[key] = pick(cleanObj[key], loggableFields[key]);
-      }
-    }
+    const cleanObj = sanitize(obj);
     pinoLogger.info(cleanObj, msg);
     addBreadcrumb({ message: msg, category: "log", level: "info", data: cleanObj });
   },
   warn: (msg: string, obj?: object) => {
-    pinoLogger.warn(obj || {}, msg);
-    addBreadcrumb({ message: msg, category: "log", level: "warning", data: obj });
+    const cleanObj = sanitize(obj);
+    pinoLogger.warn(cleanObj, msg);
+    addBreadcrumb({ message: msg, category: "log", level: "warning", data: cleanObj });
   },
   // ... repeat for error, debug, trace, fatal
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/logger.ts` around lines 49 - 93, The other logger methods
(warn, error, debug, trace, fatal) pass raw obj through while only info applies
field sanitization; extract the sanitization loop into a shared helper (e.g.,
sanitizeObj or cleanObj) and call it at the top of each method (warn, error,
debug, trace, fatal) so you pass sanitizedObj to pinoLogger.* and to
addBreadcrumb, and also correct the trace breadcrumb level to "trace" instead of
"debug"; update references to the existing
pinoLogger.warn/error/debug/trace/fatal and addBreadcrumb calls to use the
sanitized object.
apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts (2)

97-101: pullRequest typed as optional but always required at every call site — consider making it required.

All callers pass pullRequest. Keeping the parameter optional weakens type safety: a future caller that omits it would silently receive a live calculation even for CLOSED or MERGED PRs, bypassing both state guards.

♻️ Proposed fix
-  pullRequest?: PullRequest;
+  pullRequest: PullRequest;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`
around lines 97 - 101, The parameter object in the transformer that currently
types pullRequest as optional (the destructured param with from: Date | null;
duration: bigint | null; pullRequest?: PullRequest; uselessAfterMerge?: boolean)
should be changed so pullRequest is required (remove the optional marker) to
reflect that all call sites always supply it; update the type in the function
signature in pull-request-tracking.transformer.ts (and any exported
types/interfaces used there) to make pullRequest: PullRequest, then run
TypeScript to fix any leftover signatures — callers shouldn't need changes but
this enforces the invariant and prevents future omissions.

42-45: Optional: startedCodingAt ternary reads counterintuitively.

The condition firstCommitAt > pullRequest.createdAt is true when the commit is later, yet the truthy branch takes createdAt (the earlier date). The logic is correct (effectively min(firstCommitAt, createdAt)), but the reversed sense makes it easy to misread in future. A helper clarifies intent:

♻️ Proposed refactor
-  const startedCodingAt =
-    tracking.firstCommitAt && tracking.firstCommitAt > pullRequest.createdAt
-      ? pullRequest.createdAt
-      : (tracking.firstCommitAt ?? pullRequest.createdAt);
+  const startedCodingAt = tracking.firstCommitAt
+    ? new Date(Math.min(tracking.firstCommitAt.getTime(), pullRequest.createdAt.getTime()))
+    : pullRequest.createdAt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`
around lines 42 - 45, The ternary that computes startedCodingAt is written with
a reversed sense (tracking.firstCommitAt && tracking.firstCommitAt >
pullRequest.createdAt ? pullRequest.createdAt : (tracking.firstCommitAt ??
pullRequest.createdAt)) which is correct but hard to read; change it to
explicitly take the earlier of tracking.firstCommitAt and pullRequest.createdAt
(e.g., add a small helper like getEarlierDate(a,b) or use a clear min-date
expression) and then set startedCodingAt using that helper/clear expression so
the intent is obvious; update references to startedCodingAt,
tracking.firstCommitAt and pullRequest.createdAt in
pull-request-tracking.transformer.ts accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/github/services/github-pull-request.service.ts`:
- Line 212: The Octokit field response.data.merge_commit_sha is string | null,
which can leak null into the local mergeCommitSha (typed string | undefined);
change the code to normalize null to undefined (e.g. assign
response.data.merge_commit_sha ?? undefined or use a conditional ternary) before
storing into mergeCommitSha or returning it so the value becomes string |
undefined; update the return expression that currently returns
response.data.merge_commit_sha to return response.data.merge_commit_sha ??
undefined instead (or explicitly map null -> undefined wherever mergeCommitSha
is assigned).

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 56-64: The live calculation for timeToFirstApproval currently uses
tracking.firstReviewAt as the primary "from" baseline which undercounts waiting
time; change the baseline ordering in the calculateTimeForEvent call so it
prefers the PR entering the review queue (tracking.firstReadyAt) first, then
falls back to pullRequest.createdAt (and only use tracking.firstReviewAt as a
last-resort fallback if needed), keeping the existing duration
(tracking.timeToFirstApproval) and options (e.g., uselessAfterMerge: true)
intact; update the from expression in the timeToFirstApproval invocation (the
calculateTimeForEvent call) accordingly.

---

Outside diff comments:
In `@apps/web/src/app/metrics-and-insights/page.tsx`:
- Around line 171-186: The MTTR fallback strings are inconsistent with
getAbbreviatedDuration output; replace the literal "0 hours" used in the
CardDoraMetric props for amount and previousAmount with a call to
getAbbreviatedDuration(0) (or the function's zero-value equivalent) so
metrics.meanTimeToRecover?.currentAmount ? getAbbreviatedDuration(...) :
getAbbreviatedDuration(0) and likewise for previousAmount, referencing
CardDoraMetric and metrics.meanTimeToRecover to locate the code.

---

Duplicate comments:
In `@apps/api/src/app/deployment/services/deployment-pr-linking.service.ts`:
- Around line 161-182: Replace the Promise.all on filteredPullRequests with
Promise.allSettled and handle each settled result individually: call
updatePullRequestDeploymentTracking (same args: pullRequest pr as PullRequest &
{ tracking: PullRequestTracking }, deployment, workspaceId) for every PR, then
iterate the allSettled results and on any rejected result or returned
DataIntegrityException-related failure call captureException with context
(include pr and deployment/workspaceId) and do not let one failure short-circuit
others; ensure the function still awaits completion of all per-PR operations
before returning.

In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 77-82: Remove the fallback to pullRequest.createdAt in the
timeToDeploy call so that calculateTimeForEvent receives from:
pullRequest.mergedAt only; specifically update the timeToDeploy construction
(where calculateTimeForEvent is called) to pass from: pullRequest.mergedAt (not
pullRequest.mergedAt || pullRequest.createdAt) so the existing !from guard
inside calculateTimeForEvent will return null for unmerged/open PRs and avoid
computing the PR's age, keeping uselessAfterMerge: false as-is.

In `@apps/api/src/lib/logger.ts`:
- Around line 29-38: cleanObj and loggableFields cause TS index errors because
cleanObj is inferred as {} and loggableFields isn't indexed by string; update
types and add a runtime/type guard: declare cleanObj as Record<string, unknown>
(or cast obj to Record<string, unknown>) so cleanObj[key] is valid, and change
loggableFields' type to Record<string, string[]> (or assert key is keyof typeof
loggableFields) and add a guard like "if (key in loggableFields)" before
indexing; keep the existing typeof/nonnull check and continue to call
pick(cleanObj[key], loggableFields[key]) once the guards and types are fixed.
- Line 46: The Sentry breadcrumb currently gets the original unsanitized obj
(data: obj) which leaks fields excluded by loggableFields; replace that with the
sanitized object produced earlier (e.g., use the sanitized/filtered variable
returned by your sanitize/filter function) so the breadcrumb's data uses only
allowed fields, updating the code paths in the logger function where obj is
passed to Sentry (look for the breadcrumb or Sentry.addBreadcrumb call and the
obj / sanitized variable names) to pass the sanitized payload instead of obj.

---

Nitpick comments:
In
`@apps/api/src/app/pull-requests/resolvers/transformers/pull-request-tracking.transformer.ts`:
- Around line 97-101: The parameter object in the transformer that currently
types pullRequest as optional (the destructured param with from: Date | null;
duration: bigint | null; pullRequest?: PullRequest; uselessAfterMerge?: boolean)
should be changed so pullRequest is required (remove the optional marker) to
reflect that all call sites always supply it; update the type in the function
signature in pull-request-tracking.transformer.ts (and any exported
types/interfaces used there) to make pullRequest: PullRequest, then run
TypeScript to fix any leftover signatures — callers shouldn't need changes but
this enforces the invariant and prevents future omissions.
- Around line 42-45: The ternary that computes startedCodingAt is written with a
reversed sense (tracking.firstCommitAt && tracking.firstCommitAt >
pullRequest.createdAt ? pullRequest.createdAt : (tracking.firstCommitAt ??
pullRequest.createdAt)) which is correct but hard to read; change it to
explicitly take the earlier of tracking.firstCommitAt and pullRequest.createdAt
(e.g., add a small helper like getEarlierDate(a,b) or use a clear min-date
expression) and then set startedCodingAt using that helper/clear expression so
the intent is obvious; update references to startedCodingAt,
tracking.firstCommitAt and pullRequest.createdAt in
pull-request-tracking.transformer.ts accordingly.

In `@apps/api/src/lib/logger.ts`:
- Around line 49-93: The other logger methods (warn, error, debug, trace, fatal)
pass raw obj through while only info applies field sanitization; extract the
sanitization loop into a shared helper (e.g., sanitizeObj or cleanObj) and call
it at the top of each method (warn, error, debug, trace, fatal) so you pass
sanitizedObj to pinoLogger.* and to addBreadcrumb, and also correct the trace
breadcrumb level to "trace" instead of "debug"; update references to the
existing pinoLogger.warn/error/debug/trace/fatal and addBreadcrumb calls to use
the sanitized object.

Comment thread apps/api/src/app/github/services/github-pull-request.service.ts Outdated
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 18, 2026

Additional Comments (1)

apps/api/src/app/github/services/github-pull-request-tracking.service.ts
Stale JSDoc comment

The JSDoc says "Time between the first approval (or PR creation date)" but the implementation now falls through firstApprovalAt || firstReadyAt || pullRequest.createdAt. The firstReadyAt fallback is not reflected in the doc.

/**
 * Time between the first approval (or first ready date, or PR creation date) and the PR being merged
 */

Copy link
Copy Markdown
Contributor

@sweetrdev sweetrdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job

@waltergalvao waltergalvao merged commit 5c95dcf into main Feb 19, 2026
10 checks passed
@waltergalvao waltergalvao deleted the feat/pr-tracking-deployment-fields branch March 15, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

huge Huge PR - High risk of reviewer fatigue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants